/**
 * Title:        Utility Class Library<p>
 * Description:  Some useful utility classes.<p>
 * @version 1.0
 */
package com.timber.util;

import java.text.*;
import java.util.*;

public class DateFormatter extends Formatter
{
  protected DateFormat dateFormat;
  protected int formatType;
  protected TimeZone tz;

  protected static int defaultFormat = 0;
  public static final int DATEFMT_DD_MMM_YYYY      = 0;
  public static final int DATEFMT_MMDDYY           = 1;
  public static final int DATEFMT_MMDDYYYY         = 2;
  public static final int DATEFMT_YYYYMMDD         = 3;
  public static final int TIMEFMT                  = 4;
  public static final int DATETIMEFMT              = 5;
  public static final int DATETIMEFMT_SYBASE109    = 6;
  public static final int DATETIMEFMT_INT_COMPARE  = 7;
  public static final int TIMEFMT_MILLITARY        = 8;
  public static final int DATEFMT_DD_MM_YYYY       = 9;
  public static final int DATEFMT_MM_DD_YYYY       = 10;
  public static final int DATEFMT_YYYY_MM_DD       = 11;
  public static final int DATEFMT_DD_MMM_YY        = 12;


  public DateFormatter ()
  {
    this( defaultFormat );
  }

  public DateFormatter( int formatType )
  {
    this.formatType = formatType;
    switch( formatType )
    {
      case TIMEFMT:
	dateFormat = new SimpleDateFormat( "HH:mm:ss" );
	break;

      case DATEFMT_MMDDYY:
	dateFormat = new SimpleDateFormat( "MM/dd/yy" );
	break;

      case DATEFMT_DD_MMM_YYYY:
	dateFormat = new SimpleDateFormat( "dd-MMM-yyyy" );
	break;

      case DATEFMT_DD_MMM_YY:
	dateFormat = new SimpleDateFormat( "dd-MMM-yy" );
	break;

      case DATEFMT_MMDDYYYY:
	dateFormat = new SimpleDateFormat( "MM/dd/yyyy" );
	break;

      case DATEFMT_YYYYMMDD:
	dateFormat = new SimpleDateFormat( "yyyyMMdd" );
	break;

      case DATETIMEFMT:
	dateFormat = new SimpleDateFormat( "MMM dd yyyy h:mmaaa" );
	break;

      case DATETIMEFMT_SYBASE109:
	// eg. Aug 12 1999 9:35:40:124AM
	dateFormat = new SimpleDateFormat( "MMM dd yyyy h:mm:ss:SSSaaa" );
	break;

      case DATETIMEFMT_INT_COMPARE:
        // fmt:yyyyMMddHHmmssSSS
	// eg. 19991230093540124
	dateFormat = new SimpleDateFormat( "yyyyMMddHHmmssSSS" );
	break;

    case TIMEFMT_MILLITARY:
    dateFormat = new SimpleDateFormat( "HHmm" );
    break;

    case DATEFMT_DD_MM_YYYY:
    dateFormat = new SimpleDateFormat( "dd-MM-yyyy" );
    break;

    case DATEFMT_MM_DD_YYYY:
    dateFormat = new SimpleDateFormat( "MM-dd-yyyy" );
    break;

    case DATEFMT_YYYY_MM_DD:
    dateFormat = new SimpleDateFormat( "yyyy-MM-dd" );
    break;

      default:
	throw new IllegalArgumentException( "Invalid date format: " + formatType );
    }

    tz = TimeZone.getDefault();
    dateFormat.setTimeZone( tz );
  }

  public String format( GregorianCalendar gc )
  {
    Date d = gc.getTime();
    return dateFormat.format( d );
  }

  public String format( Date d )
  {
    return dateFormat.format( d );
  }

  public String format( Object o )
  {
    if (o == null)
       return "";

    if( o instanceof GregorianCalendar )
      return format( ( GregorianCalendar )o );
    else if( o instanceof Date )
      return format( ( Date )o );
    else
      throw new IllegalArgumentException( "Need Date, got " + o );
  }

  public Object parseObject( String s ) throws ParseException
  {
    return parse( s );
  }

  public GregorianCalendar parse( String s ) throws ParseException
  {
    int hh, mm;

    if( s == null || s.equals( "" ) )
      return null;

    String str = s.toUpperCase();
    if( str.startsWith( "TOD" ) )
      return new GregorianCalendar(); // Today

    if( formatType == DateFormatter.TIMEFMT )
    {
      SimpleDate date = new SimpleDate( s, formatType );
      Date tod = new Date();
      tod.setTime( date.getHour() * 3600000 +
		   date.getMinute() * 60000 +
		   date.getSeconde() * 1000 );
      GregorianCalendar gc = new GregorianCalendar();
      gc.setTime( tod );
      return gc;
    }
    else if( formatType == DateFormatter.TIMEFMT_MILLITARY )
    {
        Date date = dateFormat.parse( s );
        GregorianCalendar gc = new GregorianCalendar();
        gc.setTime( date );
        return gc;
    }
    else if( formatType == DateFormatter.DATETIMEFMT )
    {
      Date date = dateFormat.parse( s );
      GregorianCalendar gc = new GregorianCalendar();
      gc.setTime( date );
      return gc;
    }
    else if( formatType == DateFormatter.DATEFMT_DD_MM_YYYY
	     || formatType == DateFormatter.DATEFMT_DD_MMM_YYYY
	     || formatType == DateFormatter.DATEFMT_DD_MMM_YY
             || formatType == DateFormatter.DATEFMT_MM_DD_YYYY
             || formatType == DateFormatter.DATEFMT_YYYY_MM_DD)
    {
      Date date = dateFormat.parse( s );
      GregorianCalendar gc = new GregorianCalendar();
      gc.setTime( date );
      return gc;
    }
    else if( formatType == DateFormatter.DATETIMEFMT_SYBASE109 )
    {
      Date date = dateFormat.parse( s );
      GregorianCalendar gc = new GregorianCalendar();
      gc.setTime( date );
      return gc;
    }
    else
    {
      SimpleDate date = new SimpleDate( s, formatType );
      GregorianCalendar gc = new GregorianCalendar();

      gc.set( date.getYear(),
	      date.getMonth() - 1,
	      date.getDay(),
	      0,
	      0,
	      0 );

      GregorianCalendar gc2 = new GregorianCalendar();

      gc2.setTime( gc.getTime() );
      if( gc2.get( Calendar.DATE ) != date.getDay() ||
	  gc2.get( Calendar.MONTH ) != date.getMonth() - 1 ||
	  gc2.get( Calendar.YEAR ) != date.getYear() )
      {
	throw new ParseException( "Values out of range d=" +
				  date.getDay() +
				  " m=" +
				  date.getMonth() +
				  " y=" + date.getYear(),
				  0 );
      }

      gc2.set( java.util.Calendar.MILLISECOND, 0 );
      return gc2;
    }
  }

  public int getWidth ()
  {
    switch( formatType )
    {
      case TIMEFMT:
	return 8;

      case DATEFMT_MMDDYY:
	return 8;

      case DATEFMT_DD_MMM_YYYY:
      case DATEFMT_DD_MMM_YY:
	return 11;

      case DATEFMT_DD_MM_YYYY:
      case DATEFMT_MMDDYYYY:
	return 10;

      case DATEFMT_YYYYMMDD:
	return 8;

      case TIMEFMT_MILLITARY:
	return 4;

      default:
	throw new IllegalArgumentException( "Invalid date format: " + formatType );
    }
  }

  /* returns the instance string that created this instance */
  public String getInstanceKey()
  {
    return ( formatType == TIMEFMT || formatType == TIMEFMT_MILLITARY) ? "T" : "D";
  }


  /**
   * returns the class of the underlying type parsed and returned by this
   * formatter
   */
  public Class getDataType()
  {
    return Date.class;
  }

  public static java.util.Date resolveToDate(Object in)
  {
        java.util.Date ret = null;

        if (in instanceof java.util.GregorianCalendar)
           ret = ((java.util.GregorianCalendar)in).getTime();
        else if (in instanceof java.util.Date)
           ret = (java.util.Date)in;
        else if (in instanceof String) {
           DateFormatter df = new DateFormatter();
           Object obj = null;
           try {
               obj = df.parse((String)in);
           }catch(ParseException ex){}
           ret = resolveToDate( obj );
        }

        return ret;
  }
}


/**
 * Simple immutable date class to used by DateFormatter to allow ticking of the
 * date values as no suitable functionality is available in Gregorian date or Date
 *
 */
class SimpleDate
{
  private int d;
  private int m;
  private int y;
  private int hh;
  private int mm;
  private int ss;

  // Shameless copy from DateFormatter
  public static final int DATEFMT_DMY = 1;	// Europe
  public static final int DATEFMT_MDY = 2;	// America
  public static final int DATEFMT_YMD = 3;	// Japan
  public static final int TIMEFORMAT  = 4;
  public static final int DATETIMEFMT = 5;	// Japan

  private static final int numDays[]
    = { 0,31,59,90,120,151,181,212,243,273,304,334 };	// 0-based, for day-in-year
  private static final int leapNumDays[]
    = { 0,31,60,91,121,152,182,213,244,274,305,335 };	// 0-based, for day-in-year
  private static final int maxDaysInMonth[]
    = { 31,28,31,30,31,30,31,31,30,31,30,31 };	// 0-based

  /**
   * Construct with d,m,y
   */
  public SimpleDate( int d, int m, int y )
  {
    this.d = d;
    this.m = m;
    this.y = FourDigitYear( y );
    this.hh = 0;
    this.mm = 0;
    this.ss = 0;
  }

  /**
   * Construct from a Gregorian Calendar
   */
  public SimpleDate( GregorianCalendar gc )
  {
    d = gc.get( Calendar.DATE );
    m = gc.get( Calendar.MONTH ) + 1;	// gc indexes months from 0 !!!
    y = FourDigitYear( gc.get( Calendar.YEAR ) );
    hh = gc.get( Calendar.HOUR_OF_DAY );
    mm = gc.get( Calendar.MINUTE );
    ss = gc.get( Calendar.SECOND );
  }

  /**
   * Construct from a formatted string
   */
  public SimpleDate( String s, int formatType )
  {
    int p[] = getDateStringTokens( s );
    switch( formatType )
    {
      case TIMEFORMAT:
	hh = p[ 0 ];
	mm = p[ 1 ];
	ss = p[ 2 ];
	break;

      case DATEFMT_MDY:
	m = p[ 0 ];
	d = p[ 1 ];
	y = p[ 2 ];
	break;

      case DATEFMT_YMD:
	y = p[ 0 ];
	m = p[ 1 ];
	d = p[ 2 ];
	break;

      case DATETIMEFMT:
	m = p[ 0 ];
	d = p[ 1 ];
	y = p[ 2 ];
	hh = p[ 3 ];
	mm = p[ 4 ];
	ss = p[ 5 ];
	break;

      default:
	m = p[ 0 ];
	d = p[ 1 ];
	y = p[ 2 ];
	break;
    }

    if ( y >= 0 )
      y = FourDigitYear( y );
  }

  /**
   * construct from a possibly incomplete formatted String & complete from a
   * context Date
   */
  public SimpleDate (String s, int formatType, SimpleDate contextDate )
  {
    this( s, formatType );
    if( d == -1 )
      d = contextDate.d;

    if( m == -1 )
      m = contextDate.m;

    if( y == -1 )
      y = FourDigitYear( contextDate.y );

    if( hh == -1 )
      hh = contextDate.hh;

    if( mm == -1 )
      mm = contextDate.mm;

    if( ss == -1 )
      ss = contextDate.ss;
  }

  private int FourDigitYear( final int y )
  {
    int year = y;
    if( y < 49 || ( y >= 100 && y <= 999 ) )
      year += 2000;

    if( y >= 50 && y <= 99 )
      year += 1900;

    return year;
  }

  /**
   * access  day in month
   */
  public int getDay()
  {
    return d;
  }

  /**
   * access month in year
   */
  public int getMonth()
  {
    return m;
  }

  /**
   * access year
   */
  public int getYear()
  {
    return y;
  }

  public int getHour()
  {
    return hh;
  }

  public int getMinute()
  {
    return mm;
  }

  public int getSeconde()
  {
    return ss;
  }

  /**
   * get a date nm months from this one
   */
  public SimpleDate addMonths( int nm )
  {
    int ny = ( m + nm - 1 ) / 12;
    if( m + nm <= 0 )
      ny--;

    int nmd = ( m + nm - 1 ) % 12 + 1;
    if( nmd <= 0 )
      nmd = 12 + nmd;

    SimpleDate ret =  new SimpleDate( d, nmd, y + ny );
    return ret;
  }

  /**
   * get a date nd days from this one
   */
  public SimpleDate addDays( int nd )
  {
    int y = this.y;
    int m = this.m;
    int d = this.d;
    int dayInYear = calcDayInYear( d, m, y ) + nd;
    if( nd > 0 )
    {
      while( true )
      {
	int daysInYear = calcDaysInYear( y );
	if( dayInYear <= daysInYear )
	  break;

	dayInYear -= daysInYear;
	y++;
      }
    }
    else
    {
      while( true )
      {
	int daysInYear = calcDaysInYear( y );
	if( dayInYear > 0 )
	  break;

	y--;
	dayInYear += daysInYear;
      }
    }
    m = calcMonth( dayInYear, y );
    d = dayInYear - calcDaysToMonth( m, y );
    SimpleDate ret = new SimpleDate( d, m, y );
    return ret;
  }

  /**
   * get a date ny years from this one
   */
  public SimpleDate addYears( int ny )
  {
    return new SimpleDate( d, m, y + ny );
  }

  // beware: addHours() does not return a new instance as addYears(), addMonths(), addDays() do
  public SimpleDate addHours( int n )
  {
    hh += n;
    while( hh < 0 )
      hh += 24;			// those while's are because modulo opeartor %

    while( hh >= 24 )
      hh -= 24;			// is not clean on negative values

    return this;
  }

  // beware: addMinutes() does not return a new instance as addYears(), addMonths(), addDays() do
  public SimpleDate addMinutes( int n )
  {
    mm += n;
    while( mm < 0 )
    {
      mm += 60;
      hh--;
    }

    while( mm >= 60 )
    {
      mm -= 60;
      hh++;
    }
    return this;
  }

  // beware: addSecondes() does not return a new instance as addYears(), addMonths(), addDays() do
  public SimpleDate addSecondes( int n )
  {
    ss += n;
    while( ss < 0 )
    {
      ss += 60;
      mm--;
    }

    while( ss >= 60 )
    {
      ss -= 60;
      mm++;
    }
    return this;
  }

  /**
   * calc days in a give year
   */
  private static int calcDaysInYear( int y )
  {
    return ( isLeapYear( y ) ) ? 366 : 365;
  }

  /**
   * is given year a leap year
   */
  private static boolean isLeapYear( int year )
  {
    return ( ( year % 4 == 0 ) && ( ( year % 100 != 0 ) || ( year % 400 == 0 ) ) );
  }

  /**
   * get day in year 1st Jan = 1
   */
  private static int calcDayInYear( int day, int month, int year )
  {
    int ret = 0;
    if( isLeapYear( year ) )
      ret = leapNumDays[ month - 1 ] + day;
    else
      ret =  numDays[ month - 1 ] + day;

    return ret;
  }

  /**
   * get no of days in a given year before a give month
   */
  private static int calcDaysToMonth( int month, int year )
  {
    int myNumDays[] = numDays;
    if( isLeapYear( year ) )
      myNumDays = leapNumDays;
    return myNumDays[ month - 1 ];
  }

  /**
   * Calc the month in a give for a given day in the year
   */
  private static int calcMonth( int dayInYear, int year )
  {
    int myNumDays[] = numDays;
    if( isLeapYear( year ) )
      myNumDays = leapNumDays;

    for( int i = 1; i < 12; i++ )
    {
      if( dayInYear <= myNumDays[ i ] )
	return i;
    }

    return 12;
  }

  /**
   * format date into a string for a given format
   */
  public String format( int formatType )
  {
    int p1, p2, p3;

    switch( formatType )
    {
      case TIMEFORMAT:
	{
	  p1 = hh;
	  p2 = mm;
	  p3 = ss;
	  StringBuffer b = new StringBuffer();
	  b.append( hh / 10 );
	  b.append( hh % 10 );
	  b.append( ':' );
	  b.append( mm / 10 );
	  b.append( mm % 10 );
	  b.append( ':' );
	  b.append( ss / 10 );
	  b.append( ss % 10 );
	  return b.toString();
	}

      case DATEFMT_MDY:
	p1 = m;
	p2 = d;
	p3 = y;
	break;

      case DATEFMT_YMD:
	p1 = y;
	p2 = m;
	p3 = d;
	break;

      default:
	p1 = d;
	p2 = m;
	p3 = y;
    }

    return "" + p1 + "/" + p2 + "/" + p3;
  }

  /**
   * debug aid
   */
  public String toString()
  {
    return format( DATEFMT_MDY );
  }

  /**
   * return an array containing the numeric parts of a date string.
   * if any part is missing the element in the array will contain -1
   */
  private static int[] getDateStringTokens( String s )
  {
    StringTokenizer st = new StringTokenizer( s, "/:-" );
    int p[]  = new int[ 3 ];

    for( int i = 0; i < 3; i++ )
    {
      if( st.hasMoreTokens() )
	p[ i ] = Integer.valueOf( st.nextToken() ).intValue();
      else
	p[ i ] = -1;
    }
    return p;
  }
}
